{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Draw an isochrone map with OSMnx\n",
    "\n",
    "Author: [Geoff Boeing](https://geoffboeing.com/)\n",
    "\n",
    "How far can you travel on foot in 15 minutes?\n",
    "\n",
    "  - [Documentation](https://osmnx.readthedocs.io/)\n",
    "  - [Journal article and citation info](https://geoffboeing.com/publications/osmnx-paper/)\n",
    "  - [Code repository](https://github.com/gboeing/osmnx)\n",
    "  - [Examples gallery](https://github.com/gboeing/osmnx-examples)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import geopandas as gpd\n",
    "import matplotlib.pyplot as plt\n",
    "import networkx as nx\n",
    "import osmnx as ox\n",
    "from shapely.geometry import LineString\n",
    "from shapely.geometry import Point\n",
    "from shapely.geometry import Polygon\n",
    "\n",
    "ox.__version__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# configure the place, network type, trip times, and travel speed\n",
    "place = {\"city\": \"Berkeley\", \"state\": \"California\"}\n",
    "network_type = \"walk\"\n",
    "trip_times = [5, 10, 15, 20, 25]  # in minutes\n",
    "travel_speed = 4.5  # walking speed in km/hour"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Download and prep the street network"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# download the street network\n",
    "G = ox.graph_from_place(place, network_type=network_type)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# find the centermost node and then project the graph to UTM\n",
    "gdf_nodes = ox.graph_to_gdfs(G, edges=False)\n",
    "x, y = gdf_nodes[\"geometry\"].unary_union.centroid.xy\n",
    "center_node = ox.distance.nearest_nodes(G, x[0], y[0])\n",
    "G = ox.project_graph(G)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# add an edge attribute for time in minutes required to traverse each edge\n",
    "meters_per_minute = travel_speed * 1000 / 60  # km per hour to m per minute\n",
    "for _, _, _, data in G.edges(data=True, keys=True):\n",
    "    data[\"time\"] = data[\"length\"] / meters_per_minute"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Plots nodes you can reach on foot within each time\n",
    "\n",
    "How far can you walk in 5, 10, 15, 20, and 25 minutes from the origin node? We'll use NetworkX to induce a subgraph of G within each distance, based on trip time and travel speed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# get one color for each isochrone\n",
    "iso_colors = ox.plot.get_colors(n=len(trip_times), cmap=\"plasma\", start=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# color the nodes according to isochrone then plot the street network\n",
    "node_colors = {}\n",
    "for trip_time, color in zip(sorted(trip_times, reverse=True), iso_colors):\n",
    "    subgraph = nx.ego_graph(G, center_node, radius=trip_time, distance=\"time\")\n",
    "    for node in subgraph.nodes():\n",
    "        node_colors[node] = color\n",
    "nc = [node_colors[node] if node in node_colors else \"none\" for node in G.nodes()]\n",
    "ns = [15 if node in node_colors else 0 for node in G.nodes()]\n",
    "fig, ax = ox.plot_graph(\n",
    "    G,\n",
    "    node_color=nc,\n",
    "    node_size=ns,\n",
    "    node_alpha=0.8,\n",
    "    edge_linewidth=0.2,\n",
    "    edge_color=\"#999999\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Plot the time-distances as isochrones\n",
    "\n",
    "How far can you walk in 5, 10, 15, 20, and 25 minutes from the origin node? We'll use a convex hull, which isn't perfectly accurate. A concave hull would be better, but shapely doesn't offer that."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# make the isochrone polygons\n",
    "isochrone_polys = []\n",
    "for trip_time in sorted(trip_times, reverse=True):\n",
    "    subgraph = nx.ego_graph(G, center_node, radius=trip_time, distance=\"time\")\n",
    "    node_points = [Point((data[\"x\"], data[\"y\"])) for node, data in subgraph.nodes(data=True)]\n",
    "    bounding_poly = gpd.GeoSeries(node_points).unary_union.convex_hull\n",
    "    isochrone_polys.append(bounding_poly)\n",
    "gdf = gpd.GeoDataFrame(geometry=isochrone_polys)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# plot the network then add isochrones as colored polygon patches\n",
    "fig, ax = ox.plot_graph(\n",
    "    G, show=False, close=False, edge_color=\"#999999\", edge_alpha=0.2, node_size=0\n",
    ")\n",
    "gdf.plot(ax=ax, color=iso_colors, ec=\"none\", alpha=0.6, zorder=-1)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Or, plot isochrones as buffers to get more faithful isochrones than convex hulls can offer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_iso_polys(G, edge_buff=25, node_buff=50, infill=False):\n",
    "    isochrone_polys = []\n",
    "    for trip_time in sorted(trip_times, reverse=True):\n",
    "        subgraph = nx.ego_graph(G, center_node, radius=trip_time, distance=\"time\")\n",
    "\n",
    "        node_points = [Point((data[\"x\"], data[\"y\"])) for node, data in subgraph.nodes(data=True)]\n",
    "        nodes_gdf = gpd.GeoDataFrame({\"id\": list(subgraph.nodes)}, geometry=node_points)\n",
    "        nodes_gdf = nodes_gdf.set_index(\"id\")\n",
    "\n",
    "        edge_lines = []\n",
    "        for n_fr, n_to in subgraph.edges():\n",
    "            f = nodes_gdf.loc[n_fr].geometry\n",
    "            t = nodes_gdf.loc[n_to].geometry\n",
    "            edge_lookup = G.get_edge_data(n_fr, n_to)[0].get(\"geometry\", LineString([f, t]))\n",
    "            edge_lines.append(edge_lookup)\n",
    "\n",
    "        n = nodes_gdf.buffer(node_buff).geometry\n",
    "        e = gpd.GeoSeries(edge_lines).buffer(edge_buff).geometry\n",
    "        all_gs = list(n) + list(e)\n",
    "        new_iso = gpd.GeoSeries(all_gs).unary_union\n",
    "\n",
    "        # try to fill in surrounded areas so shapes will appear solid and\n",
    "        # blocks without white space inside them\n",
    "        if infill:\n",
    "            new_iso = Polygon(new_iso.exterior)\n",
    "        isochrone_polys.append(new_iso)\n",
    "    return isochrone_polys\n",
    "\n",
    "\n",
    "# make the isochrone polygons\n",
    "isochrone_polys = make_iso_polys(G, edge_buff=25, node_buff=0, infill=True)\n",
    "gdf = gpd.GeoDataFrame(geometry=isochrone_polys)\n",
    "\n",
    "# plot the network then add isochrones as colored polygon patches\n",
    "fig, ax = ox.plot_graph(\n",
    "    G, show=False, close=False, edge_color=\"#999999\", edge_alpha=0.2, node_size=0\n",
    ")\n",
    "gdf.plot(ax=ax, color=iso_colors, ec=\"none\", alpha=0.6, zorder=-1)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
